//
//  GlobalStatesStore.swift
//  MacDemo
//
//  Created by FH on 2020/12/1.
//  Copyright © 2020 FH. All rights reserved.
//

import Foundation

typealias GetStateCallbackList = [UUID : Action1<(Any, String, String)>]

public class StatesStore : StatesStoreable {
    private let mutex = Mutex()
    private var allStates: [String : Any] = [:]
    private var getStateCallbacks: [String : GetStateCallbackList] = [:]
    fileprivate let identity = UUID()
    
    var count: Int {
        return allStates.count
    }
    
    private func keyPath(_ key: String, _ sectionName: String) -> String {
        if key.count == 0 && sectionName.count > 0 {
            return "\(sectionName)."
        } else {
            return sectionName.count > 0 ? "\(sectionName).\(key)" : key
        }
    }

    public func setState(_ state: Any, byKey: String, sectionName: String) {
        let keyPath = self.keyPath(byKey, sectionName)
        allStates[keyPath] = state
    }
    
    func removeAll() {
        let succ = self.mutex.tryLock()
        self.allStates.removeAll()
        if succ {
            self.mutex.unlock()
        }
    }
    
    func removeValues(forSectionName: String) {
        let succ = self.mutex.tryLock()
        for keyPath in self.allStates.allKeys {
            let split = keyPath.components(separatedBy: ".")
            if split.count >= 2 && keyPath.starts(with: forSectionName) {
                self.allStates.removeValue(forKey: keyPath)
            }
        }
        if succ {
            self.mutex.unlock()
        }
    }
    
    func removeValue(forKey: String, sectionName: String) {
        let succ = self.mutex.tryLock()
        let keyPath = self.keyPath(forKey, sectionName)
        self.allStates.removeValue(forKey: keyPath)
        if succ {
            self.mutex.unlock()
        }
    }
    
    func updateState(_ state: Any, byKey: String, sectionName: String) {
        if byKey.count > 0 {
            let succ = mutex.tryLock()
            let keyPath = self.keyPath(byKey, sectionName)
            allStates[keyPath] = state
            
            let split = keyPath.components(separatedBy: ".")
            var keyPathPrefix = ""
            var keyPaths = [String]()
            if split.count == 1 {
                keyPaths.append(keyPath)
            } else {
                for i in 0..<split.count {
                    let item = split[i]
                    var value = ""
                    if keyPathPrefix.count == 0 {
                        keyPathPrefix = item
                    } else if i <= split.count - 2 {
                        keyPathPrefix = "\(keyPathPrefix).\(item)"
                    } else {
                        value = item
                    }
                    let path = self.keyPath(value, keyPathPrefix)
                    keyPaths.append(path)
                }
            }
            for path in keyPaths {
                if let callbackList = self.getStateCallbacks[path] {
                    for callback in callbackList.values {
                        callback((state, byKey, sectionName))
                    }
                }
            }
            if succ {
                mutex.unlock()
            }
        }
    }
    
    private func registerGetStateCallback(_ byKey: String,
                                          _ sectionName: String,
                                          _ callback: @escaping Action1<(Any, String, String)>) -> UUID {
        let succ = mutex.tryLock()
        let keyPath = self.keyPath(byKey, sectionName)
        let uuid = UUID()
        var callbackList = self.getStateCallbacks[keyPath] ?? [:]
        callbackList[uuid] = callback
        self.getStateCallbacks[keyPath] = callbackList
        if succ {
            mutex.unlock()
        }
        return uuid
    }
    
    func subscribeState(byKey: String, sectionName: String, onlyOnce: Bool=false) -> IOMonad<(Any, String, String)> {
        return IOMonad {[weak self] thunker in
            var callbackId: UUID? = nil
            if let self = self {
                callbackId = self.registerGetStateCallback(byKey, sectionName) { val in
                    thunker.sendNext((val.0, val.1, val.2))
                    if onlyOnce {
                        thunker.sendComplete()
                    }
                }
            }
            return {
                if let self = self, let callbackId = callbackId {
                    let succ = self.mutex.tryLock()
                    let keyPath = self.keyPath(byKey, sectionName)
                    if var callbackList = self.getStateCallbacks[keyPath] {
                        callbackList.removeValue(forKey: callbackId)
                        self.getStateCallbacks[keyPath] = callbackList
                    }
                    if succ {
                        self.mutex.unlock()
                    }
                }
            }
        }.debugName("StatesStore::SubscribeState")
    }
    
    func getState(_ byKey: String, sectionName: String) -> Any? {
        let keyPath = self.keyPath(byKey, sectionName)
        return allStates[keyPath]
    }
    
    func hasState(_ byKey: String, sectionName: String) -> Bool {
        let keyPath = self.keyPath(byKey, sectionName)
        return allStates[keyPath] != nil
    }
    
    func toDictionary(sectionName: String?=nil) -> [String : Any] {
        let succ = mutex.tryLock()
        defer {
            if succ {
                mutex.unlock()
            }
        }
        if let sectionName = sectionName {
            let prefixStr = "\(sectionName)."
            var states = [String : Any]()
            for (key, value) in self.allStates {
                if sectionName == "" && !key.contains(".") {
                    states[key] = value
                } else if key.hasPrefix(prefixStr) {
                    let updateKey = String(key.suffix(key.count - prefixStr.count))
                    states[updateKey] = value
                }
            }
            return states
        } else {
            return self.allStates
        }
    }

    func appendStates(states: [String : Any], sectionName: String) {
        if states.count > 0 {
            let succ = mutex.tryLock()
            for (key, value) in states {
                let keyPath = self.keyPath(key, sectionName)
                self.allStates[keyPath] = value
            }
            if succ {
                mutex.unlock()
            }
        }
    }
}

// REMARK: @PropState

protocol PropsStatesContext {
    var isBinding: Bool { get }
}

extension PropsStatesContext {
    var isBinding: Bool {
        return true
    }
}

extension PropsStatesContext {
    // for add PropsStatesContext on runtime, like StreamsInfo & StreamContext
    func bindStates(sectionName: String, statesStore: StatesStore, initStates: (String, [String : Any]) ...) {
        let initData = Array(initStates)
        let mirror = Mirror(reflecting: self)
        handleModelProperties(mirror) { properties in
            for prop in properties {
                if var propState = prop.value as? PropStatable {
                    propState.setStatesStore(statesStore: statesStore)
                    propState.sectionName = sectionName
                    if let name = prop.label {
                        propState.propName = name.getSubstring(from: 1)
                    }
                }
            }
        }
        // handle initStates
        for (section, states) in initData {
            for (keyPath, value) in states {
                statesStore.setState(value, byKey: keyPath, sectionName: section)
            }
        }
    }
    
    func bindStates(mappingSections: [String : String] = [:]) {
        self.bindStates(mappingSections: mappingSections, initState: ("", [:]))
    }
    
    func bindStates(mappingSections: [String : String] = [:],
                    initState: (String, [String : Any]),
                    _ otherStates: (String, [String : Any]) ...) {
        var initStates = Array(otherStates)
        initStates.insert(initState, at: 0)
        let statesStore = self._bindStates(mappingSections)
        // handle initStates
        for (section, states) in initStates {
            for (keyPath, value) in states {
                statesStore.setState(value, byKey: keyPath, sectionName: section)
            }
        }
    }
    
    @discardableResult
    func _bindStates(_ mappingSections: [String : String],
                     sectionName: String?=nil,
                     statesStore: StatesStore?=nil) -> StatesStore {
        let mirror = Mirror(reflecting: self)
        var localStates: StatesStore! = statesStore
        // find localStates & sections
        handleModelProperties(mirror) { properties in
            for prop in properties {
                if prop.label! == "localStates", let states = prop.value as? StatesStore {
                    localStates = states
                }

                if let section = prop.value as? PropsStatesContext, !section.isBinding {
                    let key = sectionName == nil ? prop.label! : "\(sectionName!).\(prop.label!)"
                    var sectionName = key
                    if let mappingSectionName = mappingSections[key] {
                        sectionName = mappingSectionName
                    }
                    section._bindStates(mappingSections, sectionName: sectionName, statesStore: localStates)
                }
            }
        }
        // set @propstate name
        handleModelProperties(mirror) { properties in
            for prop in properties {
                if var propState = prop.value as? PropStatable {
                    propState.setStatesStore(statesStore: localStates)
                    if let sectionName = sectionName {
                        propState.sectionName = sectionName
                    }
                    if let name = prop.label {
                        propState.propName = name.getSubstring(from: 1)
                    }
                } else if var propAction = prop.value as? PropActionable {
                    propAction.setStatesStore(statesStore: localStates)
                    if let sectionName = sectionName {
                        propAction.sectionName = sectionName
                    }
                    if let name = prop.label {
                        propAction.propName = name.getSubstring(from: 1)
                    }
                }
            }
        }
        return localStates
    }
}

protocol RepositoriesStates {
    var sectionName: String { get }
    func toDictionary() -> [String : Any]
    func resetStates(forUpdate: Bool, _ belong: StatesStore)
    func resetStates(forUpdate: Bool, _ belong: StatesStore, _ data: [String : Any])
}

extension RepositoriesStates {
    func resetStates(forUpdate: Bool, _ belong: StatesStore) {
        return self.resetStates(forUpdate: forUpdate, belong, [:])
    }
    func resetStates(forUpdate: Bool, _ belong: StatesStore, _ data: [String : Any]) {
        var sectionName: String? = nil
        var definedKeys: Set<String> = []
        let mirror = Mirror(reflecting: self)
        handleModelProperties(mirror) { properties in
            for (_, value) in properties {
                if let propState = value as? PropStatable {
                    definedKeys.insert(propState.propName)
                    if let value = data[propState.propName] {
                        if sectionName == nil {
                            sectionName = propState.sectionName
                        }
                        if forUpdate {
                            belong.updateState(value, byKey: propState.propName, sectionName: sectionName!)
                        } else {
                            belong.setState(value, byKey: propState.propName, sectionName: sectionName!)
                        }
                    } else {
                        propState.reset(forUpdate: forUpdate, belong)
                    }
                }
            }
        }
        if sectionName == nil {
            sectionName = self.sectionName
        }
        for (key, value) in data {
            if !definedKeys.contains(key) {
                if forUpdate {
                    belong.updateState(value, byKey: key, sectionName: sectionName!)
                } else {
                    belong.setState(value, byKey: key, sectionName: sectionName!)
                }
            }
        }
    }
}

private func handleModelProperties(_ mirror: Mirror, _ handleFn: (Mirror.Children) -> ()) {
    handleFn(mirror.children)
    
    if let superMirror = mirror.superclassMirror {
        handleModelProperties(superMirror, handleFn)
    }
}

private func convertEnumValue<T>(_ anyValue: Any, _ realType: Any.Type, _ valueType: T.Type) -> T? {
    // convert objc's enum value to native type, like AgoraChannelProfile.liveBroadcasting to Int Type
    if !(anyValue is T) {
        if let rawEnum = anyValue as? RawEnumConvertible, let result = rawEnum.anyRawValue as? T {
            return result
        } else if let rawEnumType = realType as? RawEnumConvertible.Type {
            // convert native value to objc's enum type, like 2 to AgoraChannelProfile Type
            if let enumValue = rawEnumType.createInstance(by: anyValue), let result = enumValue as? T {
                return result
            }
        }
    }
    return nil
}

fileprivate protocol PropStatable {
    var propName: String! { get set }
    var sectionName: String { get set }
    
    func setStatesStore(statesStore: StatesStore)
    func reset(forUpdate: Bool, _ belong: StatesStore)
}

@propertyWrapper
class PropState<T> : PropStatable {
    private weak var statesStore: StatesStore! = nil
    var isBinding: Bool {
        return self.statesStore != nil
    }
    
    fileprivate var sectionName: String = ""
    fileprivate var propName: String! {
        didSet {
            // just set init values once
            if !statesStore.hasState(propName!, sectionName: sectionName), let value = initValue {
                statesStore.updateState(value, byKey: propName!, sectionName: sectionName)
            }
        }
    }
    // use in `Trigger` state
    private let autoReset: Bool
    // help to get Optional<T>'s realType
    private let realType: Any.Type
    private var initValue: T?
    var wrappedValue: T {
        get {
            let value = self.statesStore.getState(propName!, sectionName: self.sectionName)
            if let value = value, case Optional<Any>.some(let wrappedValue) = value,
               let result = convertEnumValue(wrappedValue, self.realType, T.self) {
                return result
            } else if let type = T.self as? TypeConvertible.Type {
                return type.convert(value: value) as! T
            } else {
                return value as! T
            }
        }
    }
    
    var projectedValue: PropState {
        return self
    }
    
    fileprivate func setStatesStore(statesStore: StatesStore) {
        self.statesStore = statesStore
    }
    
    init(wrappedValue value: T, realType: Any.Type=T.self, autoReset: Bool=false) {
        self.initValue = value
        self.realType = realType
        self.autoReset = autoReset
    }
    
    func update(value: T, _ belong: StatesStore) {
        if belong.identity == self.statesStore.identity, let propName = propName {
            self.statesStore.updateState(value, byKey: propName, sectionName: self.sectionName)
        }
    }
    
    func reset(forUpdate: Bool, _ belong: StatesStore) {
        if belong.identity == self.statesStore.identity,
           let propName = propName, let value = self.initValue {
            if forUpdate {
                self.statesStore.updateState(value, byKey: propName, sectionName: self.sectionName)
            } else {
                self.statesStore.setState(value, byKey: propName, sectionName: self.sectionName)
            }
        }
    }
    
    func bindTo<ControlType: NSView>(_ control: ControlType, _ handler: @escaping (ControlType, T) -> ()) {
        var signal = self.statesStore.subscribeState(byKey: propName,
                                                     sectionName: self.sectionName)
            .take(until: control.rac_willDealloc)
        if let startValue = self.statesStore.getState(propName, sectionName: self.sectionName) {
            signal = signal.start(with: (startValue, propName, self.sectionName))
        }
        signal.subscribeNext { arg in
            let val = arg.0
            var value: T
            if let result = convertEnumValue(val, self.realType, T.self) {
                value = result
            } else if let type = T.self as? TypeConvertible.Type {
                value = type.convert(value: val) as! T
            } else {
                value = val as! T
            }
            handler(control, value)
        }
    }
    
    func bindTo<Control>(_ control: Control) where Control: NSObject & CocoaBindingDelegate {
        var signal = self.statesStore.subscribeState(byKey: propName,
                                                     sectionName: self.sectionName)
            .take(until: control.rac_willDealloc)
        if let startValue = self.statesStore.getState(propName, sectionName: self.sectionName) {
            signal = signal.start(with: (startValue, propName, self.sectionName))
        }
        signal.subscribeNext { arg in
            let val = arg.0
            var value: T
            if let result = convertEnumValue(val, self.realType, T.self) {
                value = result
            } else if let type = T.self as? TypeConvertible.Type {
                value = type.convert(value: val) as! T
            } else {
                value = val as! T
            }
            control.updateValue(value)
        }
    }
    
    func signal() -> IOMonad<T> {
        var signal = self.statesStore.subscribeState(byKey: propName, sectionName: self.sectionName)
        if let startValue = self.statesStore.getState(propName, sectionName: self.sectionName) {
            signal = signal.start(with: (startValue, propName, self.sectionName))
        }
        let realType = self.realType
        return signal.map { arg in
            let val = arg.0
            if let result = convertEnumValue(val, realType, T.self) {
                return result
            } else if let type = T.self as? TypeConvertible.Type {
                return type.convert(value: val) as! T
            } else {
                return val as! T
            }
        }.doNext {[weak self] val -> Void in
            if let self = self, self.autoReset {
                var canReset = false
                if let initValue = self.initValue {
                    let str1 = "\(val)"
                    let str2 = "\(initValue)"
                    canReset = str1 != str2
                } else {
                    canReset = true
                }
                if canReset {
                    self.reset(forUpdate: false, self.statesStore)
                }
            }
        }
    }
}

// REMARK: @PropAction

public protocol PropActionBindable {
    func startMonitoring(_ ctx: Any, _ belong: StatesStore)
}

fileprivate protocol PropActionable : PropActionBindable {
    var propName: String! { get set }
    var sectionName: String { get set }
    
    func setStatesStore(statesStore: StatesStore)
}

@propertyWrapper
class PropAction<T, Context: AnyObject> : PropActionable, Deinitable {
    var dealloc = DeallocObserver()
    private weak var statesStore: StatesStore! = nil
    fileprivate var sectionName: String = ""
    fileprivate var propName: String! {
        didSet {
            // just set init values once
            if !statesStore.hasState(propName!, sectionName: self.sectionName) {
                statesStore.updateState(self.initValue, byKey: propName!, sectionName: self.sectionName)
            }
        }
    }
    
    let initValue: T
    var wrappedValue: T {
        get {
            if let value = self.statesStore.getState(propName!, sectionName: self.sectionName) {
                if let type = T.self as? TypeConvertible.Type {
                    return type.convert(value: value) as! T
                } else {
                    return value as! T
                }
            } else {
                return initValue
            }
        }
    }
    
    var projectedValue: PropAction {
        return self
    }
    let bindingFn: Action2<T, Context>
    weak var ctx: Context! = nil
    
    init(wrappedValue value: T, _ bindingFn: @escaping Action2<T, Context>) {
        self.initValue = value
        self.bindingFn = bindingFn
    }
    
    func update(value: T, _ belong: StatesStore) {
        if belong.identity == self.statesStore.identity, let propName = propName {
            self.statesStore.updateState(value, byKey: propName, sectionName: self.sectionName)
        }
    }
    
    func startMonitoring(_ ctx: Any, _ belong: StatesStore) {
        self.startMonitoring(ctx: ctx as! Context, belong)
    }

    func startMonitoring(ctx: Context, _ belong: StatesStore) {
        if self.statesStore.identity == belong.identity {
            self.ctx = ctx
            let signal = self.statesStore.subscribeState(byKey: propName, sectionName: self.sectionName)
                .take(until: self.rac_willDealloc)
            signal.subscribeNext {[weak self] arg in
                let val = arg.0
                if let self = self, let ctx = self.ctx {
                    var value: T
                    if let type = T.self as? TypeConvertible.Type {
                        value = type.convert(value: val) as! T
                    } else {
                        value = val as! T
                    }
                    call(self.bindingFn, with: (value, ctx))
                }
            }
        }
    }
    
    fileprivate func setStatesStore(statesStore: StatesStore) {
        self.statesStore = statesStore
    }
}
